/**
 * Copyright 2006 Brad Reynolds
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.leeloo.databinding.ui.event;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.URLDecoder;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Parses a string in dot (.) notation.
 * 
 * @author Brad Reynolds
 */
public class EventParser {
    private final static Pattern CLASS_NAME_PATTERN = Pattern.compile("className=(.*)"); //$NON-NLS-1$

    private final static Pattern NAME_VALUE_PATTERN = Pattern.compile("(.*)=(.*)"); //$NON-NLS-1$

    private final static String DELIMETER = "\t"; //$NON-NLS-1$
    
    /**
     * Sets the <code>propertyName</code> to the <code>value</code> on the
     * provided <code>object</code>.
     * 
     * @param object
     * @param propertyName
     * @param value
     *            String or Integer are currently supported. If instanceof
     *            String the value will be URL decoded.
     */
    private void setProperty(Object object, String propertyName, String value) {
        PropertyDescriptor propertyDescriptor;
        try {
            propertyDescriptor = new PropertyDescriptor(propertyName, object.getClass());
            Method readMethod = propertyDescriptor.getReadMethod();

            if (readMethod.getReturnType().equals(int.class)) {
                propertyDescriptor.getWriteMethod().invoke(object, new Object[] { new Integer(value) });
            } else if (readMethod.getReturnType().equals(String.class)) {
                propertyDescriptor.getWriteMethod().invoke(object, new Object[] { URLDecoder.decode(value, "UTF-8") }); //$NON-NLS-1$
            }
        } catch (Exception e) {
            // rethrow exception as unchecked exception
            throw new RuntimeException(e);
        }
    }

    /**
     * Retrieves the object with the provided <code>propertyName</code>. If
     * the object is <code>null</code> it is created and set on the
     * <code>parent</code>.
     * 
     * @param parent
     * @param propertyName
     * @return object for property
     */
    private Object getObject(Object parent, String propertyName) {
        Object result = null;
        try {
            PropertyDescriptor objectPropertyDescriptor = new PropertyDescriptor(propertyName, parent.getClass());
            Method readMethod = objectPropertyDescriptor.getReadMethod();
            result = readMethod.invoke(parent, null);

            if (result == null) {
                Class returnType = readMethod.getReturnType();
                Constructor constructor = returnType.getConstructor(null);
                result = constructor.newInstance(null);
                objectPropertyDescriptor.getWriteMethod().invoke(parent, new Object[] { result });
            }
        } catch (Exception e) {
            // rethrow exception as unchecked exception
            throw new RuntimeException(e);
        }

        return result;
    }

    /**
     * Parses a string into java objects. The format is like an HTTP GET except
     * the name value pairs are separated separated by tab. The reason for using
     * tab and not &amp; is that the message comes across the wire as XML and
     * &amp; will get encoded. The first name value pair must be "className"
     * with the fully qualified name of the object to be the root.
     * 
     * @param message
     * @return instance of the object specified in the className name value pair
     * @throws IllegalArgumentException
     *             if the first name value pair is not
     *             "className=name.of.class.to.instantiate".
     */
    public IEvent parse(String message) {
        IEvent root = null;

        StringTokenizer tokenizer = new StringTokenizer(message, DELIMETER);
        for (int i = 0; tokenizer.hasMoreTokens(); i++) {
            String token = tokenizer.nextToken();

            if (i == 0) {
                // First parameter must be the className so that the root
                // object can be constructed before properties are set.
                Matcher matcher = CLASS_NAME_PATTERN.matcher(token);
                if (!matcher.matches()) {
                    throw new IllegalArgumentException("First name value pair must be 'className=name.of.class.to.instantiate'."); //$NON-NLS-1$
                }

                String value = matcher.group(1);

                Class rootClass;
                try {
                    rootClass = Class.forName(value, false, getClass().getClassLoader());
                    Constructor constructor = rootClass.getConstructor(null);
                    root = (IEvent) constructor.newInstance(null);
                } catch (Exception e) {
                    // rethrow exception as unchecked exception
                    throw new RuntimeException(e);
                }
            } else {
                Matcher matcher = NAME_VALUE_PATTERN.matcher(token);
                if (!matcher.matches()) {
                    throw new IllegalArgumentException("Token [" + token + "] is invalid."); //$NON-NLS-1$//$NON-NLS-2$
                }

                String name = matcher.group(1);
                String value = matcher.group(2);

                int index = name.indexOf('.');
                int start = 0;
                Object parent = root;
                while (index > -1) {
                    String objectName = name.substring(start, index);
                    parent = getObject(parent, objectName);

                    start = index + 1;
                    index = name.indexOf('.', start);
                }

                index = name.lastIndexOf('.');
                String propertyName = null;
                if (index > -1) {
                    propertyName = name.substring(index + 1);
                } else {
                    propertyName = name;
                }

                setProperty(parent, propertyName, value);
            }
        }

        return root;
    }
}
